#!/usr/bin/env python
""" AnyBackup is a windows specific backup program to keep multiple volumes in sync with multiple volumes
    Copyright (C) 2011  Andrew Hopkinson

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

    Exported Classes:

    backupManager -- the backup class for AnyBackup, it contains the drive objects and meta data
    """


from file import file
from drive import drive
import ConfigParser
import DriveUtil
import sys
import re
import shutil
import os
from operator import attrgetter

def file_cmp(a,b):
    return cmp(a.getRoot().lower(),b.getRoot().lower()) or cmp(a.getPath().lower(),b.getPath().lower()) or cmp(a.getName().lower(),b.getName().lower())

class backupManager:
    """ the backup class for AnyBackup, it contains the drive objects and meta data """
    BACKUP=2
    CONTENT=3
    ALL=4
    DUPLICATE=8
    UNNAMED=7
    INVALIDCONFIG=6
    MISSING=9
    BALANCED='balanced'
    STICKY='sticky'
    version = '0.8'

    def __init__(self):
        self._backupDrives = []
        self._contentDrives = []
        self._skipList = []
        self._validExt = []
        self.readConfigs()
        
    def toggleDrive(self,driveRef):
        if driveRef.isBackup():
            self._backupDrives.remove(driveRef)
            self._contentDrives.append(driveRef)
            driveRef.setIsBackup(False)
        else:
            self._contentDrives.remove(driveRef)
            self._backupDrives.append(driveRef)
            driveRef.setIsBackup(True)
        return True

    def readConfigs(self):
        config = ConfigParser.RawConfigParser()
        config.read('AnyBackup.cfg')
        try:
            backupMode = config.get('Backup','drive-selection')
            if backupMode == self.BALANCED or backupMode == self.STICKY:
                self.backupMode = backupMode
            else:
                self.backupMode = self.BALANCED
                return self.INVALIDCONFIG
        except:
            self.backupMode = self.BALANCED
            return self.INVALIDCONFIG
        return True

    def getBackupMode(self):
        return self.backupMode

    def sortFiles(self,files):
        files.sort(file_cmp)
        return files

    def getDrive(self,fileRef):
        for driveRef in self._backupDrives + self._contentDrives:
            if fileRef.getVolName() == driveRef.getName() and fileRef.getSerial() == driveRef.getSerial():
                return driveRef
        return None
    
    def remoteIndexDrive(self,driveRef):
        for driveObj in self._contentDrives:
            if driveRef.getName() == driveObj.getName() and driveRef.getSerial() == driveObj.getSerial():
                return driveObj.remoteIndex()

    def refreshDrive(self,driveRef,guiObj=None):
        for driveObj in self._backupDrives + self._contentDrives:
            if driveRef.getName() == driveObj.getName() and driveRef.getSerial() == driveObj.getSerial():
                return driveObj.indexDrive(guiObj)

    def refreshDrives(self,type):
        if type is None:
            type = backupManager.ALL
        drives = []
        if type == backupManager.ALL or type == backupManager.BACKUP:
            drives += self._backupDrives
        if type == backupManager.ALL or type == backupManager.CONTENT:
            drives += self._contentDrives
        for driveRef in drives:
            print "Refreshing " + driveRef.getName() + " - " + driveRef.getSerial() + "..."
            driveRef.indexDrive()

    def getSkipList(self):
        return self._skipList

    def addSkip(self, skip):
        if skip:
            self._skipList.append(skip)
            for driveRef in self._contentDrives + self._backupDrives:
                driveRef.setSkipList(self.getSkipList())

    def setSkipList(self,skip):
        if skip:
            self._skipList = skip
            for driveRef in self._contentDrives + self._backupDrives:
                driveRef.setSkipList(skip)

    def getValidExt(self):
        return self._validExt

    def addValidExt(self,ext):
        if ext:
            self._validExt.append(ext)
            for driveRef in self._contentDrives + self._backupDrives:
                driveRef.setValidExt(self.getValidExt())

    def setValidExt(self,ext):
        if ext:
            self._validExt = ext
            for driveRef in self._contentDrives + self._backupDrives:
                driveRef.setValidExt(ext)

    def addBackupDrive(self,letter,guiObj=None,index=True):
        return self.addDrive(letter,backupManager.BACKUP,guiObj,index)

    def addContentDrive(self,letter,paths,guiObj=None,index=True):
        return self.addDrive(letter,backupManager.CONTENT,guiObj,index,paths)

    def removeDrive(self,driveIn):
        if driveIn is None:
            print "Undefined drive reference, check your selection."
            return False
        for driveRef in self._backupDrives[:] + self._contentDrives[:]:
            if driveRef.getName() == driveIn.getName() and driveRef.getSerial() == driveIn.getSerial():
                try:
                    self._backupDrives.remove(driveRef)
                except ValueError:
                    self._contentDrives.remove(driveRef)
                return True
        return False

    def setBackupDrives(self, backupDrives):
        if backupDrives:
            self._backupDrives = backupDrives

    def setContentDrives(self, contentDrives):
        if contentDrives:
            self._contentDrives = contentDrives

    def getBackupDrives(self):
        return self._backupDrives

    def getContentDrives(self):
        return self._contentDrives

    def searchFiles(self,regex):
        results = []
        for driveRef in self._contentDrives + self._backupDrives:
            driveResults = driveRef.search(regex)
            if driveResults:
                results += driveResults
        return results

    def uniqueCheck(self,name,serial):
        for driveRef in self._contentDrives + self._backupDrives:
            if driveRef.getName() == name and driveRef.getSerial() == serial:
                print "You've already added " + name + " - " + serial + "!"
                return False
            elif driveRef.getName() == name:
                print "WARNING: " + name + " -- You've got two drives with the same Volume Name -- this is a bad idea! (Repent!)"
        return True

    def addDrive(self,letter,type,guiObj=None,index=True,paths=None):
        name = DriveUtil.getName(letter)
        serial = DriveUtil.getSerial(letter)
        totalSpace = DriveUtil.getTotalSpace(letter)
        freeSpace = DriveUtil.getFreeSpace(letter)
        if not name:
            return self.UNNAMED
        if name and serial and totalSpace and freeSpace:
            isBackup = False;
            if type == backupManager.BACKUP:
                isBackup = True
            if paths:
                newDrive = drive(name,serial,totalSpace,freeSpace,isBackup,paths=[])
                for path in paths:
                    newDrive.addDir(path)
            else:
                newDrive = drive(name,serial,totalSpace,freeSpace,isBackup)
            newDrive.setValidExt(self._validExt)
            newDrive.setSkipList(self._skipList)
            if not self.uniqueCheck(name,serial):
                return backupManager.DUPLICATE
            if index:
                newDrive.indexDrive(guiObj)
            if type == backupManager.BACKUP:
                self._backupDrives.append(newDrive)
            elif type == backupManager.CONTENT:
                self._contentDrives.append(newDrive)
            return True
        return False

    def hashify(self,fileIn,skiplist,hash,):
        for fileRef in fileIn.getFileList():
            key = fileRef.getPath() + os.sep + fileRef.getName()
            key = self.escapeString(key)
            hash[key]=fileRef
        return hash

    def escapeString(self,str):
        str = re.sub("\?","",str)
        str = re.sub("\+","",str)
        str = re.sub("\[","",str)
        str = re.sub("\-","",str)
        str = re.sub("\&","",str)
        str = re.sub("\]","",str)
        str = re.sub("\(","",str)
        str = re.sub("\)","",str)
        str = re.sub("\{","",str)
        str = re.sub("\}","",str)
        while re.search(r'^%s'%re.escape(os.sep),str,re.IGNORECASE):
            str = re.sub(r'^%s'%re.escape(os.sep),"",str)
        return str.lower()

    def reduceHash(self,fileIn,hash):
        for fileRef in fileIn.getFileList():
            key = fileRef.getPath() + os.sep + fileRef.getName()
            key = self.escapeString(key)
            if hash.has_key(key):
                if int(hash[key].getMTime()) != int(fileRef.getMTime()) or hash[key].getSize() != fileRef.getSize():
                    continue
                del hash[key]
        return hash

    def listNewFiles(self,fileRef):
        output = True

    def getNewFiles(self):
        fileHash = {}
        files = []
        skipList = '|'.join(self._skipList)
        for driveRef in self._contentDrives:
            if driveRef.getRootFile():
                fileHash = self.hashify(driveRef.getRootFile(),skipList,fileHash)
        for driveRef in self._backupDrives:
            if driveRef.getRootFile():
                fileHash = self.reduceHash(driveRef.getRootFile(),fileHash)
        files = fileHash.values()
        files = self.sortFiles(files)
        return files

    def getOldFiles(self):
        fileHash = {}
        files = []
        skipList = '|'.join(self._skipList)
        for driveRef in self._backupDrives:
            if driveRef.getRootFile():
                fileHash = self.hashify(driveRef.getRootFile(),skipList,fileHash)
        for driveRef in self._contentDrives:
            if driveRef.getRootFile():
                fileHash = self.reduceHash(driveRef.getRootFile(),fileHash)
        files = fileHash.values()
        files = self.sortFiles(files)
        return files

    def getBackupDrive(self,fileRef):
        if self.backupMode == self.BALANCED:
            return self.getMostFreeDrive(self.BACKUP)
        else:
            for driveRef in self._backupDrives:
                if driveRef.findDir(fileRef.getPath()):
                    return driveRef
        return None

    def getContentDrive(self,fileRef):
        for driveRef in self._contentDrives:
            if driveRef.findDir(fileRef.getPath()):
                return driveRef
        return None

    def getMostFreeDrive(self,driveType):
        if not driveType:
            driveType = backupManager.BACKUP
        mostFree = 0
        mostDrive = None
        drives = []
        if driveType == backupManager.BACKUP:
            drives = self._backupDrives
        elif driveType == backupManager.CONTENT:
            drives = self._contentDrives
        for driveRef in drives:
            if driveRef.isBackup():
                if driveRef.isLocked():
                    continue
            freeSpace = driveRef.getFreeSpace()*1024 - driveRef.getPendingWrite()
            if freeSpace > mostFree:
                mostDrive = driveRef
                mostFree = freeSpace
        return mostDrive

    def backupNewFiles(self):
        missingDrives = {}
        drivesToRefresh = {}
        files = self.getNewFiles()
        toStore = {}
        print "Sorting new files..."
        for driveRef in self.getContentDrives() + self.getBackupDrives():
            driveRef.clearPendingWrite()
        for fileRef in files:
            buDrive = self.getBackupDrive(fileRef)
            if buDrive:
                if not buDrive.addPendingWrite(fileRef):
                    buDrive = None
            if not buDrive:
                buDrive = self.getMostFreeDrive(backupManager.BACKUP)
            if buDrive:
                if not buDrive.addPendingWrite(fileRef):
                    buDrive = None
            if not buDrive:
                print "Eror getting an open drive, no backup drives? All backup drives full?"
                return
            if buDrive.getName()+buDrive.getSerial() not in toStore:
                toStore[buDrive.getName()+buDrive.getSerial()] = {}
                toStore[buDrive.getName()+buDrive.getSerial()]['files'] = []
            toStore[buDrive.getName()+buDrive.getSerial()]['drive'] = buDrive
            toStore[buDrive.getName()+buDrive.getSerial()]['files'].append(fileRef)
        for id in toStore.keys():
            buRef = toStore[id]['drive']
            for fileRef in toStore[id]['files']:
                print "Backing up " + fileRef.getName() + "..."
                sourceSerial = fileRef.getSerial()
                sourceName = fileRef.getVolumeName()
                sourceLetter = DriveUtil.getLetter(sourceName,sourceSerial)
                if not sourceLetter:
                    print "Content drive " + sourceName + " - " + sourceSerial + " is not connected!"
                    continue
                backupLetter = None
                backupLetter = DriveUtil.getLetter(buRef.getName(),buRef.getSerial())
                if buRef.getName()+buRef.getSerial() in missingDrives:
                    print "Skipping, " + buRef.getName() + " - " + buRef.getSerial() + " not connected!"
                    continue
                while not backupLetter:
                    print buRef.getName() + " - " + buRef.getSerial + "is not connected, either connect it now and enter Y[es] to retry or enter N[o] to cancel: (Y/N)"
                    input = sys.stdin.readline()
                    if re.match("^Y$",input,re.IGNORECASE) or re.match("^Yes$",input,re.IGNORECASE):
                        backupLetter = DriveUtil.getLetter(buRef.getName(),buRef.getSerial())
                    elif re.match("^N$",input,re.IGNORECASE) or re.match("^No$",input,re.IGNORECASE):
                        missingDrives[buRef.getName()+buRef.getSerial()] = buRef
                        continue
                if buRef.getFreeSpace()*1048576 < fileRef.getSize():
                    print buRef.getName() + " - " + buRef.getSerial()+ " does not have enough free space, skipping file."
                    continue

                source = os.path.join(sourceLetter+":\\",fileRef.getPath(),fileRef.getName())
                targetDir = os.path.join(backupLetter+":\\",fileRef.getPath())
                if not os.path.exists(targetDir):
                    try:
                        os.makedirs(targetDir)
                    except:
                        print "Unexpected error:", sys.exc_info()[0]
                        continue
                try:
                    shutil.copy2(source,os.path.join(targetDir,fileRef.getName()))
                except IOError, e:
                    print "Unable to copy file. %s" % e
                    continue
                buName = buRef.getName()
                drivesToRefresh[buName]=buRef
                buRef.updateFreeSpace()
        for driveRef in missingDrives.keys():
            print "Must connect " + driveRef.getName()
        for driveRef in drivesToRefresh.keys():
            print "Refreshing " + driveRef + "..."
            drivesToRefresh[driveRef].indexDrive()